/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client.caller;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import jp.fores.midori.client.RPCClient;
import jp.fores.midori.client.exception.AllTargetRPCFailureException;
import jp.fores.midori.server.exception.MidoriServerUnRecoveryException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * 複数のサーバーに対して並列にリモートプロシージャーコールを行うようにするための実装クラス。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class ParallelRPCClientCaller extends AbstractRPCClientCaller {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(ParallelRPCClientCaller.class);

	/**
	 * 並列実行するスレッド数のデフォルト値
	 */
	protected static final int DEFAULT_PARALLEL_THREAD_COUNT = 5;


	//==========================================================
	//フィールド

	/**
	 * リモートプロシージャーコールを行うクライアントクラス
	 */
	protected final RPCClient rpcClient;

	/**
	 * 並列実行するスレッド数
	 */
	protected final int parallelThreadCount;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 *
	 * @param rpcClient リモートプロシージャーコールを行うクライアントクラス
	 */
	public ParallelRPCClientCaller(RPCClient rpcClient) {
		//実際に処理を行うコンストラクタを呼び出す
		//(並列実行するスレッド数にはデフォルト値の定数を指定する)
		this(rpcClient, DEFAULT_PARALLEL_THREAD_COUNT);
	}

	/**
	 * 並列実行するスレッド数を指定するタイプのコンストラクタです。<br>
	 *
	 * @param rpcClient リモートプロシージャーコールを行うクライアントクラス
	 * @param parallelThreadCount 並列実行するスレッド数
	 * @throws IllegalArgumentException 並列実行するスレッド数に0以下の値を指定した場合
	 */
	public ParallelRPCClientCaller(RPCClient rpcClient, int parallelThreadCount)
			throws IllegalArgumentException {
		//==========================================================
		//引数のチェック

		//並列実行するスレッド数が0以下の場合
		if (parallelThreadCount <= 0) {
			//不正な引数の場合の例外を投げる
			throw new IllegalArgumentException("並列実行するスレッド数に0以下の値を指定することはできません。");
		}


		//==========================================================
		//引数の値をフィールドに設定

		//リモートプロシージャーコールを行うクライアントクラス
		this.rpcClient = rpcClient;

		//並列実行するスレッド数
		this.parallelThreadCount = parallelThreadCount;
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object remoteCall(URLListGetter urlListGetter, String methodName,
			Object... args) throws Exception {
		//==========================================================
		//サーバーのURLのリストを取得
		List<String> urlList = urlListGetter.getURLList();

		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("取得したサーバーのURLのリスト:" + urlList);
		}

		//サーバーのURLのリストが取得できなかった場合
		if ((urlList == null) || (urlList.isEmpty())) {
			//全てのRPCの対象に対して呼び出しが失敗した場合の例外を投げる
			throw new AllTargetRPCFailureException("サーバーのURLのリストが取得できませんでした。");
		}


		//==========================================================
		//取得したサーバーのURLのリストに対して並列にリモートプロシージャーコールを行う
		//(並列に実行するため、ExecutorServiceを使って別スレッドを立ち上げる)

		//タスクのリスト
		//(初期容量はサーバーのURLのリストのサイズに合わせておく)
		List<Callable<Object>> taskList = new ArrayList<Callable<Object>>(urlList.size());

		//サーバーのURLのリストに対してループをまわす
		for (String url : urlList) {
			//作業用のタスククラス(インナークラス)のインスタンスを生成し、タスクのリストに追加する
			taskList.add(new InternalCallableTask(this.rpcClient, url, methodName, args));
		}

		//指定されたスレッド数で並列動作するExecutorServiceのインスタンスを取得
		//(スレッド数にはフィールドの並列実行するスレッド数と、タスクのリストのサイズの小さい方の値を指定する)
		ExecutorService ex = Executors.newFixedThreadPool(Math.min(this.parallelThreadCount, taskList.size()));

		try {
			//タイムアウト指定なしでタスクのリストをそれぞれ並列に実行し、正常に完了したタスクの値を返す
			//(全てのタスクが失敗した場合は例外が発生する)
			Object result = ex.invokeAny(taskList);

			//タスクの戻り値が回復不能な例外の場合
			if (result instanceof MidoriServerUnRecoveryException) {
				//戻り値として返すのでなく、例外として投げる
				throw (MidoriServerUnRecoveryException) result;
			}

			//タスクの戻り値を返す
			return result;
		}
		//正常に完了したタスクがない場合
		catch (ExecutionException e) {
			//全てのRPCの対象に対して呼び出しが失敗した場合の例外を投げる
			throw new AllTargetRPCFailureException("全てのサーバーのURLに対してリモートプロシージャーコールを行いましたが、全て失敗しました。");
		}
		//終了処理
		finally {
			//Executorをシャットダウンして、以降のタスクが実行されないようにする
			ex.shutdownNow();
		}

	}


	//==========================================================
	//インナークラス

	/**
	 * 作業用のタスククラス。
	 * (インラインの無名クラスでも良かったのですが、少し処理が複雑なのでprivateな内部クラスとして定義しています。)
	 */
	private static class InternalCallableTask implements Callable<Object> {
		//==========================================================
		//フィールド

		/**
		 * リモートプロシージャーコールを行うクライアントクラス
		 */
		private final RPCClient rpcClient;

		/**
		 * URL
		 */
		private final String url;

		/**
		 * メソッド名
		 */
		private final String methodName;

		/**
		 * メソッドに渡す引数
		 */
		private final Object[] args;


		//==========================================================
		//メソッド

		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//コンストラクタ

		/**
		 * コンストラクタです。<br>
		 *
		 * @param rpcClient リモートプロシージャーコールを行うクライアントクラス
		 * @param url URL
		 * @param methodName メソッド名
		 * @param args メソッドに渡す引数
		 */
		public InternalCallableTask(RPCClient rpcClient, String url,
				String methodName, Object[] args) {
			//==========================================================
			//引数の値をフィールドに設定

			//リモートプロシージャーコールを行うクライアントクラス
			this.rpcClient = rpcClient;

			//URL
			this.url = url;

			//メソッド名
			this.methodName = methodName;

			//メソッドに渡す引数
			this.args = args;
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//インターフェースの実装

		/**
		 * フィールドの情報を元にして、リモートプロシージャーコール用を行い、結果を返します。
		 * 回復不能な例外が発生した場合は、それ以上不毛な処理を行わないようにするため、捕捉した例外オブジェクトを返します。<br>
		 *
		 * @return リモートプロシージャコールの戻り値のオブジェクト(戻り値が存在しない場合はnull)
		 * @throws Exception 例外
		 */
		@Override
		public Object call() throws Exception {
			try {
				//実際にリモートプロシージャーコールを行い、結果をそのまま返す
				return this.rpcClient.remoteCall(this.url, this.methodName, this.args);
			}
			//例外が発生した場合
			catch (Exception e) {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("リモートプロシージャコールの途中で例外が発生しました。", e);
				}

				//サーバー側で回復不能な例外が発生した場合
				if (e instanceof MidoriServerUnRecoveryException) {
					//補足した例外を返す
					//(これ以上不毛な処理を行わないようにするため)
					return e;
				}
				//それ以外の例外の場合
				else {
					//補足した例外をそのまま投げる
					throw e;
				}
			}
		}
	}
}
